package Dungeon;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import be.kuleuven.cs.som.annotate.*;

import Border.Border;
import Border.Door;
import SquarePackage.Direction;
import SquarePackage.Square;
import SquarePackage.TeleportableSquare;
import SquarePackage.Rock;

/**
 * Class for a dungeon.
 * 
 * @invar	All coordinates are positive and smaller than the maximum value.
 * 			| isValidDimension()
 *  
 * @author 	Brecht && Florian
 * @version	1.0
 */
public class Dungeon extends Subdungeon{
	
	/**
	 * Initializes a new dungeon with the given coordinates for the maximum coordinate
	 * 
	 * @param	maxX
	 * 			The maximum x-coordinate value.
	 * @param	maxY
	 * 			The maximum y-coordinate value.
	 * @param	maxZ
	 * 			The maximum z-coordinate value. 			
	 * @effect	The given coordinates are the coordinates of the maximum dimension.
	 * 			| Dimension(maxX, maxY, maxZ) == new.getMaxDimension() 
	 */
	public Dungeon(long maxX,long maxY, long maxZ){
		super(maxX, maxY, maxZ);
		this.rooms = new HashSet<Room>();
	}
	
	/**
	 * Initializes a new dungeon with the given dimension as maximum dimension.
	 * 
	 * @param 	maxDimension
	 * 			The maximum dimension
	 * @effect	The given maximum dimension is the new max dimension.
	 * 			| new.getMaxDimension = maxDimension
	 */
	public Dungeon(Dimension maxDimension){
		super(maxDimension);
		this.rooms = new HashSet<Room>();
	}
	
	/**
	 * Initializes a default dungeon.
	 * 
	 * @effect	The dungeon is initialized with 20,20,20 as maximum dimension.
	 * 			| this(new Dimension(20,20,20))
	 */
	public Dungeon(){
		this(20,20,20);
	}
		
	/**
	 * Set registering the used dimensions.
	 */
	private Set<Dimension> usedDimensions = new HashSet<Dimension>();
	
	/**
	 * Map registering the coordinates of the squares and the squares.
	 */
	private Set<Room> rooms;
	
	/**
	 * Return the used dimensions.
	 */
	@Basic @Raw
	public Set<Dimension> getUsedDimensions(){
		return usedDimensions; 
	}
	
	/**
	 * Return the set of rooms in this dungeon.
	 */
	@Basic @Raw
	public Set<Room> getRooms(){
		return rooms;
	}
	
	/**
	 * Add the given square on the given dimension.
	 * 
	 * @param 	square
	 * 			The square to add.
	 * @param 	dimension
	 * 			The dimension where the square should be added.
	 * @post	The square is added at the given dimension.
	 * 			| getAllSquares().contains(square)
	 * 			| getAllDimensions().contains(dimension)
	 * @throws 	IllegalArgumentException
	 * 			One of the arguments is not a valid argument.
	 * 			| !isValidDimension()||!isValidSquare()
	 */
	@Model
	public void addSquare(Square square, Dimension dimension)throws IllegalArgumentException{
		if(!isValidDimension(dimension))
			throw new IllegalArgumentException("not a valid dimension");
		if(!isValidSquare(square))
			throw new IllegalArgumentException("not a valid square");
		rooms.add(new Room(this, square, dimension));
		usedDimensions.add(dimension);
		for(Direction direction: Direction.values()){
			Square other = getSquareAt(dimension.getDimensionIn(direction));
			if(other!=null){
				if(!square.getBorderAt(direction).getIsolation()&&!other.getBorderAt(direction.getOpposite()).getIsolation()){
					getRoomOf(other).mergeWith(getRoomOf(square));
				}
				setBorder(square, other, direction);
			}
		}
		updateRocks();
	}
	/**
	 * updates the temperature and humidity of the rocks and rooms of size 1(mostly rocks) in this dungeon
	 * @post all the rooms of size 1 are now in a correct state
	 */
	private void updateRocks(){
		for(Room room: getRooms())
			if(room.getNumberOfSquares()==1){
				room.updateTemperatureAndHumidity();
			}
	}
	
	/**
	 * Sets the border of a border between two squares in the dungeon
	 * 
	 * @param 	square
	 * 			The first square.
	 * @param 	other
	 * 			The second square.
	 * @param 	direction
	 * 			The direction in which the squares are added.
	 * @post	The first square and the second square share the same border with the highest isolation.
	 * 			| square.getBorderAt(direction).equals(other.getBorderAt(direction.other))
	 */
	private void setBorder(Square square, Square other, Direction direction){
		Border border = square.getBorderAt(direction).getHighest(other.getBorderAt(direction.getOpposite()));
		if(square instanceof Rock){
			square.getBorderAt(direction).setSquare(square);
		}
		else{
			((TeleportableSquare)square).setBorder(direction, border);
		}
		if(other instanceof Rock){
			Direction direction2= direction.getOpposite();
			Border border2 = other.getBorderAt(direction2);
			((Rock)other).replaceBorderWith(border2, border);
			other.getBorderAt(direction2).setSquare(other);
		}
		else{
			((TeleportableSquare)other).setBorder(direction.getOpposite(), border);
		}
	}
	
	/**
	 * Checks whether this dimension is a valid dimension.
	 * 
	 * @param 	dimension
	 * 			The given dimension to check.
	 * @return	True if and only if the given dimension is made for this dungeon
	 * 		  | result = dimension.getDungeon().equals(this)
	 */
	private boolean isValidDimension(Dimension dimension){
		if(dimension == null)
			return false;
		if(dimension.getXPosition()==dimension.getYPosition() && dimension.getZPosition()==dimension.getXPosition())
			return false;
		if(dimension.getXPosition()>getMaxDimension().getXPosition() && dimension.getYPosition()>getMaxDimension().getYPosition() && dimension.getZPosition()>getMaxDimension().getZPosition())
			return false;
		return true;
	}
	
	/**
	 * Checks whether the given square is a valid square.
	 * 
	 * @param	square
	 * 			The given square to check.
	 * @return	True if and only if the square is a valid square.
	 * 			| if((square == null)&&(coordinates.containsValue(square)))
	 * 			| 	result = false
	 * 			| else if(square.getMadeOfSlipperyMaterial())
	 * 			| 	result = getSlipperyPercent() < 20 
	 * 			| else result = true
	 */
	private boolean isValidSquare(Square square){
		if(square == null){
			return false;
		}
		if(hasBeenUsed(square)){
			return false;
		}
		if(square.getMadeOfSlipperyMaterial()){
			return (getSlipperyPercent()<20);
		}
		return true;
		}
	
	/**
	 * Return the percent of slippery floors in this dungeon.
	 */
	private int getSlipperyPercent(){
		int slipperiness = 1;
		for(Square square: getAllSquares()){
			if(square.getMadeOfSlipperyMaterial())
				slipperiness++;
		}
		System.out.println("" + slipperiness * 100 / getAllSquares().size() + "");
		int slipperypercent = (slipperiness * 100) / (getAllSquares().size()+1);
		return slipperypercent;
	}
	
	/**
	 * Return the square on the given dimension.
	 * 
	 * @param	dimension
	 * 			The dimension to find the square.
	 * @return	The square at the given position. 
	 * 			The result could be null if the given position doesn't contain a square.
	 */
	public Square getSquareAt(Dimension dimension){
		for(Room room: getRooms()){
			Square result = room.getSquareAt(dimension);
			if(result != null)
				return result;
		}
		return null;
	}
	
	/**
	 * Remove the given square at the given dimension.
	 * 
	 * @param	dimension
	 * 			The given dimension where the square must be removed.
	 * @post	The square at the given position isn't a part of the dungeon anymore.
	 * 		  	| getSquareAt(dimension)=null
	 */
	@Model
	public void removeSquareAt(Dimension dimension) throws IllegalArgumentException{
		if(!isValidDimension(dimension))
			throw new IllegalArgumentException("not a valid dimension");
		Room room = getRoomOf(dimension);
//		Room room2 = (Room) room.clone(); 
		room.removeSquareAt(dimension);
		for(Direction direction: Direction.values()){
			Dimension startDimension = dimension.getDimensionIn(direction);
			if(room.contains(startDimension)){
				HashMap<Dimension, Square> neighbourSquares = (HashMap<Dimension, Square>) room.getNeighbourSquares(startDimension);
				Room room2 = new Room(this, neighbourSquares);
				room.removeDoubleSquares(room2);
				rooms.add(room2);
				if(room.getNumberOfSquares()==0)
					removeRoom(room);
			}
		}
	}
	
	/**
	 * checks whether the first square is in the same space as the second square.
	 * 
	 * @param	firstSquare 
	 * 			The first square.
	 * @param	otherSquare
	 * 			The second square.
	 * @return	True if and only the first square is in the same room as the second square.
	 * 		  	| result = room.contains(firstSquare)&& room.contains(secondSquare)
	 * @note	TIMECOMPLEXITY = O(m)+O(n) with n = number of squares in the room,
	 * 											m = number of squares in the dungeon.
	 */
	public boolean isInSameRoomAs(Square firstSquare, Square otherSquare){
		Room room = getRoomOf(firstSquare);
		if(room!= null){
			return room.contains(otherSquare);
		}
		return false;
	}
	
	/**
	 * Return all the squares used in this dungeon.
	 * 
	 * @return	Set of all the used squares in this dungeon.
	 */
	public Set<Square> getAllSquares(){
		Set<Square> allSquares = new HashSet<Square>();
		for(Room room: getRooms())	{
			allSquares.addAll(room.getAllSquares());
		}
		return allSquares;
	}
	
	/**
	 * Checker whether the given square has been used or not.
	 * 
	 * @param 	square
	 * 			The square to check.
	 * @return	True if and only if the given square has already been used in this dungeon.
	 * 		  	| getAllSquares().contains(square)
	 */
	public boolean hasBeenUsed(Square square){
		return getAllSquares().contains(square);
	}
	
	/**
	 * Checks whether the given dimension isn't used yet.
	 * 
	 * @param 	dimension
	 * 			The given dimension to verify.
	 * @return	True if and only if the dimension isn't used yet.
	 * 		  	| result = !getAllDimension().contains(dimension)
	 * @note	TIMECOMPLEXITY = O(1) because a hashset is used.
	 */
	public boolean isAvailable(Dimension dimension){
		return !getUsedDimensions().contains(dimension);
	}
	
	/**
	 * Remove the given room from the set of rooms.
	 * 
	 * @param 	room
	 * 			The room we want to remove.
	 * @post	The given room doesn't exist anymore.
	 * 			| !getRooms().contains(room)
	 */
	public void removeRoom(Room room){
		rooms.remove(room);
		room.terminate();	
	}
	
	public void removeDoubleRoom(Room room, Room room2){
		rooms.remove(room);
		room2.removeDoubleSquares(room);
		room.terminate();
	}
	
	/**
	 * Returns the room which the given square contains.
	 * 
	 * @param 	square
	 * 			The square to know the room of.
	 * @return	The room which contains the given square.
	 */
	public Room getRoomOf(Square square){
		for(Room room: getRooms()){
			if(room.contains(square))
				return room;
		}
		return null;
	}
	
	/**
	 * Returns the room which the given dimension contains.
	 * 
	 * @param 	dimension
	 * 			The dimension to know the room of.
	 * @return	The room which contains the given dimension.
	 */
	public Room getRoomOf(Dimension dimension){
		for(Room room: getRooms()){
			if(room.contains(dimension))
				return room;
		}
		return null;
	}
	
	/**
	 * Returns a collection of squares that are in the same room as the given square.
	 * 
	 * @param	square
	 * 			The square we want to have all the neighbouring squares of.
	 * @return	A collection of squares that are in the same room as the given square
	 * 			| result = getRoomOf(square).getAllDimensions()
	 */
	public Collection<Square> GetAllSquaresOfRoomOf(Square square){
		Room room = getRoomOf(square);
		if(room == null)
			return null;
		return room.getAllSquares();
	}
	
	/**
	 * Opens a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @effect	The rooms on both sides of the door get merged.
	 * 			| room1.mergeWith(room2)
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this room.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model
	public void openDoor(Square square, Direction direction)throws IllegalArgumentException{
		Dimension dimension = getDimensionOf(square);
		if(dimension == null)
			throw new IllegalArgumentException("Not part of this dungeon");
		if(!isDoor(square,direction))
			throw new IllegalArgumentException("Not a door.");
		getRoomOf(square.getBorderAt(direction).getIdentifier()).mergeWith(getRoomOf(square.getBorderAt(direction).getSecondSquare()));
		((Door) square.getBorderAt(direction)).open();
		updateRocks();
	}
	
	/**
	 * Closes a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @post	The rooms on both sides of the door get separated if needed.
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this room.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model
	public void closeDoor(Square square, Direction direction)throws IllegalArgumentException{
		Dimension dimension = getDimensionOf(square);
		if(dimension == null)
			throw new IllegalArgumentException("Not part of this dungeon");
		if(!isDoor(square,direction))
			throw new IllegalArgumentException("Not a door.");
		((Door) square.getBorderAt(direction)).close();
		HashMap<Dimension, Square> neighbourSquares = new HashMap<Dimension, Square>();
		Room room = getRoomOf(square);
		neighbourSquares = (HashMap<Dimension, Square>) room.getNeighbourSquares(getDimensionOf(square), neighbourSquares);
		Room room2 = new Room(this, neighbourSquares);
		room2.removeDoubleSquares(room);
		rooms.add(room2);
		if(room.getNumberOfSquares()==0)
			removeRoom(room);
	}
		
	/**
	 * Checks whether there is a door at the given square in the given direction
	 * 
	 * @param	square
	 * 			Square to check.
	 * @param	direction
	 *			Direction to check.
	 * @return	True if and only if there is a door in the given direction in the given square.
	 * 		  	| result = square.getBorderAt(direction) instanceof Door
	 */
	private boolean isDoor(Square square, Direction direction){
		return (square.getBorderAt(direction).canBeOpenedOrClosed());
	}
	
	/**
	 * Return the dimension of the given square.
	 * 
	 * @param 	square
	 * 			The square of which the dimension must be known.
	 * @return	The dimension of the given square.
	 */
	protected Dimension getDimensionOf(Square square){
		for(Room room: getRooms()){
			Dimension dimension = room.getDimensionOf(square);
			if(dimension != null)
				return dimension;
		}
		return null;
	}
}